'use strict';
const Cesium = require('cesium');
const fsExtra = require('fs-extra');
const loadMtl = require('../../lib/loadMtl');
const loadTexture = require('../../lib/loadTexture');
const obj2gltf = require('../../lib/obj2gltf');

const clone = Cesium.clone;

const coloredMaterialPath = 'specs/data/box/box.mtl';
const texturedMaterialPath = 'specs/data/box-complex-material/box-complex-material.mtl';
const texturedWithOptionsMaterialPath = 'specs/data/box-texture-options/box-texture-options.mtl';
const multipleMaterialsPath = 'specs/data/box-multiple-materials/box-multiple-materials.mtl';
const externalMaterialPath = 'specs/data/box-external-resources/box-external-resources.mtl';
const resourcesInRootMaterialPath = 'specs/data/box-resources-in-root/box-resources-in-root.mtl';
const externalInRootMaterialPath = 'specs/data/box-external-resources-in-root/box-external-resources-in-root.mtl';
const transparentMaterialPath = 'specs/data/box-transparent/box-transparent.mtl';
const sharedTexturesMaterialPath = 'specs/data/box-shared-textures/box-shared-textures.mtl';
const sharedTexturesMaterial2Path = 'specs/data/box-shared-textures-2/box-shared-textures-2.mtl';

const diffuseTexturePath = 'specs/data/box-textured/cesium.png';
const transparentDiffuseTexturePath = 'specs/data/box-complex-material/diffuse.png';
const alphaTexturePath = 'specs/data/box-complex-material-alpha/alpha.png';
const ambientTexturePath = 'specs/data/box-complex-material/ambient.gif';
const normalTexturePath = 'specs/data/box-complex-material/bump.png';
const emissiveTexturePath = 'specs/data/box-complex-material/emission.jpg';
const specularTexturePath = 'specs/data/box-complex-material/specular.jpeg';
const specularShininessTexturePath = 'specs/data/box-complex-material/shininess.png';

let diffuseTexture;
let transparentDiffuseTexture;
let alphaTexture;
let ambientTexture;
let normalTexture;
let emissiveTexture;
let specularTexture;
let specularShininessTexture;

const checkTransparencyOptions = {
    checkTransparency : true
};
const decodeOptions = {
    decode : true
};

let options;

describe('loadMtl', () => {
    beforeAll(async () => {
        diffuseTexture = await loadTexture(diffuseTexturePath, decodeOptions);
        transparentDiffuseTexture = await loadTexture(transparentDiffuseTexturePath, checkTransparencyOptions);
        alphaTexture = await loadTexture(alphaTexturePath, decodeOptions);
        ambientTexture = await loadTexture(ambientTexturePath);
        normalTexture = await loadTexture(normalTexturePath);
        emissiveTexture = await loadTexture(emissiveTexturePath);
        specularTexture = await loadTexture(specularTexturePath, decodeOptions);
        specularShininessTexture = await loadTexture(specularShininessTexturePath, decodeOptions);
    });

    beforeEach(() => {
        options = clone(obj2gltf.defaults);
        options.overridingTextures = {};
        options.logger = () => {};
    });

    it('loads mtl', async () => {
        options.metallicRoughness = true;
        const materials = await loadMtl(coloredMaterialPath, options);
        expect(materials.length).toBe(1);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture).toBeUndefined();
        expect(pbr.metallicRoughnessTexture).toBeUndefined();
        expect(pbr.baseColorFactor).toEqual([0.64, 0.64, 0.64, 1.0]);
        expect(pbr.metallicFactor).toBe(0.5);
        expect(pbr.roughnessFactor).toBe(96.078431);
        expect(material.name).toBe('Material');
        expect(material.emissiveTexture).toBeUndefined();
        expect(material.normalTexture).toBeUndefined();
        expect(material.ambientTexture).toBeUndefined();
        expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.1]);
        expect(material.alphaMode).toBe('OPAQUE');
        expect(material.doubleSided).toBe(false);
    });

    it('loads mtl with textures', async () => {
        options.metallicRoughness = true;
        const materials = await loadMtl(texturedMaterialPath, options);
        expect(materials.length).toBe(1);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture).toBeDefined();
        expect(pbr.metallicRoughnessTexture).toBeDefined();
        expect(pbr.baseColorFactor).toEqual([1.0, 1.0, 1.0, 0.9]);
        expect(pbr.metallicFactor).toBe(1.0);
        expect(pbr.roughnessFactor).toBe(1.0);
        expect(material.name).toBe('Material');
        expect(material.emissiveTexture).toBeDefined();
        expect(material.normalTexture).toBeDefined();
        expect(material.occlusionTexture).toBeDefined();
        expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]);
        expect(material.alphaMode).toBe('BLEND');
        expect(material.doubleSided).toBe(true);
    });

    it('loads mtl with textures having options', async () => {
        options.metallicRoughness = true;
        const materials = await loadMtl(texturedWithOptionsMaterialPath, options);
        expect(materials.length).toBe(1);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture).toBeDefined();
        expect(pbr.metallicRoughnessTexture).toBeDefined();
        expect(pbr.baseColorFactor).toEqual([1.0, 1.0, 1.0, 0.9]);
        expect(pbr.metallicFactor).toBe(1.0);
        expect(pbr.roughnessFactor).toBe(1.0);
        expect(material.name).toBe('Material');
        expect(material.emissiveTexture).toBeDefined();
        expect(material.normalTexture).toBeDefined();
        expect(material.occlusionTexture).toBeDefined();
        expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]);
        expect(material.alphaMode).toBe('BLEND');
        expect(material.doubleSided).toBe(true);
    });

    it('loads mtl with multiple materials', async () => {
        options.metallicRoughness = true;
        const materials = await loadMtl(multipleMaterialsPath, options);
        expect(materials.length).toBe(3);
        expect(materials[0].name).toBe('Blue');
        expect(materials[0].pbrMetallicRoughness.baseColorFactor).toEqual([0.0, 0.0, 0.64, 1.0]);
        expect(materials[1].name).toBe('Green');
        expect(materials[1].pbrMetallicRoughness.baseColorFactor).toEqual([0.0, 0.64, 0.0, 1.0]);
        expect(materials[2].name).toBe('Red');
        expect(materials[2].pbrMetallicRoughness.baseColorFactor).toEqual([0.64, 0.0, 0.0, 1.0]);
    });

    it('sets overriding textures', async () => {
        spyOn(fsExtra, 'readFile').and.callThrough();
        options.overridingTextures = {
            metallicRoughnessOcclusionTexture : alphaTexturePath,
            baseColorTexture : alphaTexturePath,
            emissiveTexture : emissiveTexturePath
        };
        const materials = await loadMtl(texturedMaterialPath, options);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture.name).toBe('alpha');
        expect(pbr.metallicRoughnessTexture.name).toBe('alpha');
        expect(material.emissiveTexture.name).toBe('emission');
        expect(material.normalTexture.name).toBe('bump');
        expect(fsExtra.readFile.calls.count()).toBe(3);
    });

    it('loads texture outside of the mtl directory', async () => {
        const materials = await loadMtl(externalMaterialPath, options);
        const material = materials[0];
        const baseColorTexture = material.pbrMetallicRoughness.baseColorTexture;
        expect(baseColorTexture.source).toBeDefined();
        expect(baseColorTexture.name).toBe('cesium');
    });

    it('does not load texture outside of the mtl directory when secure is true', async () => {
        const spy = jasmine.createSpy('logger');
        options.logger = spy;
        options.secure = true;

        const materials = await loadMtl(externalMaterialPath, options);
        const material = materials[0];
        const baseColorTexture = material.pbrMetallicRoughness.baseColorTexture;
        expect(baseColorTexture).toBeUndefined();
        expect(spy.calls.argsFor(0)[0].indexOf('Texture file is outside of the mtl directory and the secure flag is true. Attempting to read the texture file from within the obj directory instead') >= 0).toBe(true);
        expect(spy.calls.argsFor(1)[0].indexOf('ENOENT') >= 0).toBe(true);
        expect(spy.calls.argsFor(2)[0].indexOf('Could not read texture file') >= 0).toBe(true);
    });

    it('loads textures from root directory when the texture paths do not exist', async () => {
        const materials = await loadMtl(resourcesInRootMaterialPath, options);
        const material = materials[0];
        const baseColorTexture = material.pbrMetallicRoughness.baseColorTexture;
        expect(baseColorTexture.source).toBeDefined();
        expect(baseColorTexture.name).toBe('cesium');
    });

    it('loads textures from root directory when texture is outside of the mtl directory and secure is true', async () => {
        options.secure = true;

        const materials = await loadMtl(externalInRootMaterialPath, options);
        const material = materials[0];
        const baseColorTexture = material.pbrMetallicRoughness.baseColorTexture;
        expect(baseColorTexture.source).toBeDefined();
        expect(baseColorTexture.name).toBe('cesium');
    });

    it('alpha of 0.0 is treated as 1.0', async () => {
        const materials = await loadMtl(transparentMaterialPath, options);
        expect(materials.length).toBe(1);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture).toBeUndefined();
        expect(pbr.metallicRoughnessTexture).toBeUndefined();
        expect(pbr.baseColorFactor[3]).toEqual(1.0);
        expect(material.alphaMode).toBe('OPAQUE');
        expect(material.doubleSided).toBe(false);
    });

    it('ambient texture is ignored if it is the same as the diffuse texture', async () => {
        const materials = await loadMtl(sharedTexturesMaterialPath, options);
        expect(materials.length).toBe(1);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture).toBeDefined();
        expect(pbr.occlusionTexture).toBeUndefined();
    });

    it('texture referenced by specular is decoded', async () => {
        const materials = await loadMtl(sharedTexturesMaterialPath, options);
        expect(materials.length).toBe(1);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture.pixels).toBeDefined();
        expect(pbr.baseColorTexture.source).toBeDefined();
        expect(pbr.metallicRoughnessTexture.pixels).toBeDefined();
        expect(pbr.metallicRoughnessTexture.source).toBeUndefined();
    });

    it('texture referenced by diffuse and emissive is not decoded', async () => {
        const materials = await loadMtl(sharedTexturesMaterial2Path, options);
        expect(materials.length).toBe(1);
        const material = materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(pbr.baseColorTexture).toBe(material.emissiveTexture);
        expect(pbr.baseColorTexture.pixels).toBeUndefined();
        expect(pbr.baseColorTexture.source).toBeDefined();
    });

    describe('metallicRoughness', () => {
        it('creates default material', () => {
            const material = loadMtl._createMaterial(undefined, options);
            const pbr = material.pbrMetallicRoughness;
            expect(pbr.baseColorTexture).toBeUndefined();
            expect(pbr.metallicRoughnessTexture).toBeUndefined();
            expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]);
            expect(pbr.metallicFactor).toBe(0.0); // No metallic
            expect(pbr.roughnessFactor).toBe(1.0); // Fully rough
            expect(material.emissiveTexture).toBeUndefined();
            expect(material.normalTexture).toBeUndefined();
            expect(material.ambientTexture).toBeUndefined();
            expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]);
            expect(material.alphaMode).toBe('OPAQUE');
            expect(material.doubleSided).toBe(false);
        });

        it('creates material with textures', () => {
            options.metallicRoughness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : diffuseTexture,
                ambientTexture : ambientTexture,
                normalTexture : normalTexture,
                emissiveTexture : emissiveTexture,
                specularTexture : specularTexture,
                specularShininessTexture : specularShininessTexture
            }, options);

            const pbr = material.pbrMetallicRoughness;
            expect(pbr.baseColorTexture).toBeDefined();
            expect(pbr.metallicRoughnessTexture).toBeDefined();
            expect(pbr.baseColorFactor).toEqual([1.0, 1.0, 1.0, 1.0]);
            expect(pbr.metallicFactor).toBe(1.0);
            expect(pbr.roughnessFactor).toBe(1.0);
            expect(material.emissiveTexture).toBeDefined();
            expect(material.normalTexture).toBeDefined();
            expect(material.occlusionTexture).toBeDefined();
            expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]);
            expect(material.alphaMode).toBe('OPAQUE');
            expect(material.doubleSided).toBe(false);
        });

        it('packs occlusion in metallic roughness texture', () => {
            options.metallicRoughness = true;
            options.packOcclusion = true;

            const material = loadMtl._createMaterial({
                ambientTexture : alphaTexture,
                specularTexture : specularTexture,
                specularShininessTexture : specularShininessTexture
            }, options);

            const pbr = material.pbrMetallicRoughness;
            expect(pbr.metallicRoughnessTexture).toBeDefined();
            expect(pbr.metallicRoughnessTexture).toBe(material.occlusionTexture);
        });

        it('does not create metallic roughness texture if decoded texture data is not available', () => {
            options.metallicRoughness = true;
            options.packOcclusion = true;

            const material = loadMtl._createMaterial({
                ambientTexture : ambientTexture, // Is a .gif which can't be decoded
                specularTexture : specularTexture,
                specularShininessTexture : specularShininessTexture
            }, options);

            const pbr = material.pbrMetallicRoughness;
            expect(pbr.metallicRoughnessTexture).toBeUndefined();
            expect(material.occlusionTexture).toBeUndefined();
        });

        it('sets material for transparent diffuse texture', () => {
            options.metallicRoughness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : transparentDiffuseTexture
            }, options);
            expect(material.alphaMode).toBe('BLEND');
            expect(material.doubleSided).toBe(true);
        });

        it('packs alpha texture in base color texture', () => {
            options.metallicRoughness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : diffuseTexture,
                alphaTexture : alphaTexture
            }, options);

            const pbr = material.pbrMetallicRoughness;
            expect(pbr.baseColorTexture).toBeDefined();

            let hasBlack = false;
            let hasWhite = false;
            const pixels = pbr.baseColorTexture.pixels;
            const pixelsLength = pixels.length / 4;
            for (let i = 0; i < pixelsLength; ++i) {
                const alpha = pixels[i * 4 + 3];
                hasBlack = hasBlack || (alpha === 0);
                hasWhite = hasWhite || (alpha === 255);
            }
            expect(hasBlack).toBe(true);
            expect(hasWhite).toBe(true);
            expect(pbr.baseColorFactor[3]).toEqual(1);
            expect(material.alphaMode).toBe('BLEND');
            expect(material.doubleSided).toBe(true);
        });

        it('uses diffuse texture if diffuse and alpha are the same', () => {
            options.metallicRoughness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : diffuseTexture,
                alphaTexture : diffuseTexture
            }, options);

            const pbr = material.pbrMetallicRoughness;
            expect(pbr.baseColorTexture).toBe(diffuseTexture);
            expect(material.alphaMode).toBe('BLEND');
            expect(material.doubleSided).toBe(true);
        });
    });

    describe('specularGlossiness', () => {
        it('creates default material', () => {
            options.specularGlossiness = true;
            const material = loadMtl._createMaterial(undefined, options);
            const pbr = material.extensions.KHR_materials_pbrSpecularGlossiness;
            expect(pbr.diffuseTexture).toBeUndefined();
            expect(pbr.specularGlossinessTexture).toBeUndefined();
            expect(pbr.diffuseFactor).toEqual([0.5, 0.5, 0.5, 1.0]);
            expect(pbr.specularFactor).toEqual([0.0, 0.0, 0.0]); // No specular color
            expect(pbr.glossinessFactor).toEqual(0.0); // Rough surface
            expect(material.emissiveTexture).toBeUndefined();
            expect(material.normalTexture).toBeUndefined();
            expect(material.occlusionTexture).toBeUndefined();
            expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]);
            expect(material.alphaMode).toBe('OPAQUE');
            expect(material.doubleSided).toBe(false);
        });

        it('creates material with textures', () => {
            options.specularGlossiness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : diffuseTexture,
                ambientTexture : ambientTexture,
                normalTexture : normalTexture,
                emissiveTexture : emissiveTexture,
                specularTexture : specularTexture,
                specularShininessTexture : specularShininessTexture
            }, options);

            const pbr = material.extensions.KHR_materials_pbrSpecularGlossiness;
            expect(pbr.diffuseTexture).toBeDefined();
            expect(pbr.specularGlossinessTexture).toBeDefined();
            expect(pbr.diffuseFactor).toEqual([1.0, 1.0, 1.0, 1.0]);
            expect(pbr.specularFactor).toEqual([1.0, 1.0, 1.0]);
            expect(pbr.glossinessFactor).toEqual(1.0);
            expect(material.emissiveTexture).toBeDefined();
            expect(material.normalTexture).toBeDefined();
            expect(material.occlusionTexture).toBeDefined();
            expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]);
            expect(material.alphaMode).toBe('OPAQUE');
            expect(material.doubleSided).toBe(false);
        });

        it('does not create specular glossiness texture if decoded texture data is not available', () => {
            options.specularGlossiness = true;

            const material = loadMtl._createMaterial({
                specularTexture : ambientTexture, // Is a .gif which can't be decoded
                specularShininessTexture : specularShininessTexture
            }, options);

            const pbr = material.extensions.KHR_materials_pbrSpecularGlossiness;
            expect(pbr.specularGlossinessTexture).toBeUndefined();
        });

        it('sets material for transparent diffuse texture', () => {
            options.specularGlossiness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : transparentDiffuseTexture
            }, options);

            expect(material.alphaMode).toBe('BLEND');
            expect(material.doubleSided).toBe(true);
        });

        it('packs alpha texture in diffuse texture', () => {
            options.specularGlossiness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : diffuseTexture,
                alphaTexture : alphaTexture
            }, options);

            const pbr = material.extensions.KHR_materials_pbrSpecularGlossiness;
            expect(pbr.diffuseTexture).toBeDefined();

            let hasBlack = false;
            let hasWhite = false;
            const pixels = pbr.diffuseTexture.pixels;
            const pixelsLength = pixels.length / 4;
            for (let i = 0; i < pixelsLength; ++i) {
                const alpha = pixels[i * 4 + 3];
                hasBlack = hasBlack || (alpha === 0);
                hasWhite = hasWhite || (alpha === 255);
            }
            expect(hasBlack).toBe(true);
            expect(hasWhite).toBe(true);
            expect(pbr.diffuseFactor[3]).toEqual(1);
            expect(material.alphaMode).toBe('BLEND');
            expect(material.doubleSided).toBe(true);
        });

        it('uses diffuse texture if diffuse and alpha are the same', () => {
            options.specularGlossiness = true;

            const material = loadMtl._createMaterial({
                diffuseTexture : diffuseTexture,
                alphaTexture : diffuseTexture
            }, options);

            const pbr = material.extensions.KHR_materials_pbrSpecularGlossiness;
            expect(pbr.diffuseTexture).toEqual(diffuseTexture);
            expect(material.alphaMode).toBe('BLEND');
            expect(material.doubleSided).toBe(true);
        });
    });
});
